#!/usr/local/bin/python
# Rob Dennis - Lurchington Q. Delacroix
# Keasby - TV renamer
# Project Homepage: http://code.google.com/p/keasby/
import logging
import os
import re
import unittest
from string import Template
from os.path import join as pjoin
from base_classes import KeasbyTestCase
from keasby_toolbox import reshape_dict
from keasby_toolbox import split_path,choose_single,cut_from_string
from keasby_toolbox import reshape_item
from keasby_options import keasby_cl

class MediaInterpreter():
    """
    Parses information from existing files, and interacts with those files
    """
    def __init__(self, media_list=[]):

        self._initialize_media_info()

        self.seed_media_list(media_list)

#---Initialization
    def seed_media_list(self, media_list):
        """
        seperates initialization of media_list from other init
        """

        if not isinstance(media_list, list):
            media_list = [media_list]

        self.media_list = self._initialize_filelist(media_list)

    def _initialize_filelist(self, media_list):

        return [self._initialize_filename_info(file) for file in media_list]


    def _initialize_filename_info(self, file):

        tdict = {}

        tdict["path"] = file
        tdict["filename"],tdict["ext"] = os.path.splitext(
                                                    os.path.basename(file))

        tdict["parent_of_parent_dir"],tdict["parent_dir"] = split_path(file)


        return tdict


    def _initialize_media_info(self):
        """
        Initializes information used by the entire interpreter
        """

        self._initialize_search_regex()
        self.errors = {}

    def _initialize_search_regex(self):
        """
        initializes search MediaRegex instances and set access points
        """
        self.search_regex = []

        self.search_regex.append(ShownameRegex())
        self.showname_regex=self.search_regex[0]

        self.search_regex.append(SeasonRegex())
        self.season_regex=self.search_regex[1]

        self.search_regex.append(EpisodeRegex())
        self.episode_regex=self.search_regex[2]

#---Interaction
##    def get_tvepisodes(self, media_list=None):
##        """
##        Intended to be the primary interface, returns a list of TvEpisode
##        instances based on the provided media_list
##        """
##
##        if media_list:
##            self.seed_media_list(media_list)
##
##        return (self._merge_tvepisodes(self.repo.get_tvepisode(groups))
##                        for groups in self.get_groups_list())
##
##    def _merge_tvepisodes(self, tv_episode_list):
##        """
##        Merges more than one TvEpisode to convey a grouped episode.
##        """
##
##        return tv_episode_list[0]

    def _get_matches_list(self):
        """
        returns the list of MatchObjects associated for all files in
        media_list
        """
        return map(self._get_matches, self.media_list)


    def _get_matches(self,media_file):
        """
        Returns the MatchObjects associated with a given media_file.
        One for each regex in self.search_regex. If a given regex
        doesn't have a valid MatchObject, then it will append None
        instead.
        """
        matches = []
        temp_forwarding_dict = {}


        for regex in self.search_regex:
            regex.seed_dict(media_file,temp_forwarding_dict)

            try:
                match = regex.get_best_match()
            except MediaRegexError,error_message:
                logging.warning("unable to get match: %s",error_message)
                match = None

            temp_forwarding_dict = regex.get_forwarding_info()
            matches.append(match)

        return matches

    def get_groups_list(self,media_list=None):
        """
        returns the list of the groups found by get_groups for all files
        in the pre-seeded media_list
        """

        if media_list is not None:
            self.seed_media_list(media_list)

        return map(self.get_groups,self.media_list)

    def get_groups(self,media_file):
        """
        Returns the combined group dictionary of a given media_file, also
        includes the path of the original media_file. Expects a string or
        an initialized dict as returned by seed_media_list. If a string,
        it will be run through the seed_media_list process before operating
        """

        if not isinstance(media_file,dict):
            if isinstance(media_file,basestring):
                media_file = self._initialize_filename_info(media_file)
            else:
                _fail = " ".join(["incorrect %s"%type(media_file),
                                    "specified for get_groups,",
                                    "need dict or basestring"])

                raise MediaInterpreterError(_fail)

        tdict = {}

        for match_object in filter(None,self._get_matches(media_file)):
            for key,value in match_object.groupdict().items():
                reshaped_key,reshaped_value = reshape_item(key,value)
                tdict[reshaped_key] = reshaped_value

        tdict["path"] = media_file["path"]

        return tdict

    def _seed_regex_list(self,media_file):
        """
        convenience function for seed all the MediaRegex instances
        """
        for regex in self.search_regex:
            regex.seed_dict(media_file)

#---Error Reporting

    def has_error(self,error_key):
        """
        Returns the error messages associated with the given key
        """
        return self.errors.get(error_key)

    def _add_error(self,error_key,error_msg):

        if error_key not in self.errors:
            self.errors[error_key] = []

        self.errors.append(error_msg)



class MediaRegex():
    """
    Used to group a set of regex operations, operating on a path/filename
    """

    def __init__(self):

        self.regex = []
        self.forward_dict = {}

        self.seed_dict(sep = "\\"+os.sep)

#---Set Information
    def seed_dict(self,*dict_args,**seeded_information):
        """
        Sets the information needed to construct regular expressions
        """
        for arg in dict_args:
            if isinstance(arg,dict):
                self.__dict__.update(arg)

        self.__dict__.update(seeded_information)

    def _add_regex(self,pattern,string,flags=0):
        """
        Adds a regular expression to test against, expands a string
        Template formatting to be expanded later with the information
        from seed_dict
        """
        self.regex.append((pattern,string,flags))

    def _update_forwarding_info(self,*dict_info,**forwarded_info):
        """
        Some MediaRegex expressions are dependant on information found in
        a previous expression. This will update the information that the
        MediaRegex will forward to subsequent MediaRegex
        """
        for dict in dict_info:
            self.forward_dict.update(dict)

        self.forward_dict.update(forwarded_info)

    def _set_forwarded_info(self,match):
        """
        Intended to be overloaded by subclasses, will update the information
        each subclass sets as forwardable.
        """
        pass


#---Return Information
    def get_best_match(self):
        """
        Returns the best regular expression match, raise a MediaRegexError
        if there's no valid choice
        """
        choice = choose_single(self._get_match_list())

        if choice is None:
            raise MediaRegexError("%s: unable to find a match for %s"%(
                                                    self.__class__.__name__,
                                                self.__dict__.get("path"))
                                                    )

        if choice is not None:
            self._set_forwarded_info(choice)

        return choice

    def _expand_template(self,string,format_dict):
        """
        expands a string.Template, will throw a MediaRegexError if there are
        any unsubsituted parameters
        """

        try:
            string = Template(string).substitute(format_dict)
        except KeyError, error_message:
            logging.debug("Error with formatting string %s: %s",
                            string,
                            error_message)

            raise MediaRegexError,error_message

        logging.debug("successfully expanded: %s",string)

        return string

    def _get_match(self,regex,string,flags,regex_function=re.search):
        """
        Returns a match object for a given tuple of regular expression
        pattern, the string to test against, and flags to use. Expressions
        and strings are cast as string.Templates that expect to be
        substituted.
        """

        try:
            string = self._expand_template(string,self.__dict__)
        except MediaRegexError, error_message:
            logging.debug("reverting to old string: %s",string)

        try:
            regex = self._expand_template(regex,self.__dict__)
        except MediaRegexError, error_message:
            logging.debug("reverting to old regex: %s",regex)

        logging.debug('regex(r"%s","%s",%s)',regex,string,flags)

        if not string:
            logging.debug("empty string, skipping")
            return None

        match = regex_function(regex,string,flags)

        try:
            logging.debug(match.groupdict())
        except AttributeError:
            logging.debug("no MatchObject")

        return match


    def _get_match_list(self,regex_list=None):
        """
        Groups together the matches, returning a list.
        """

        if regex_list is None:
            regex_list = self.regex

        matchobject_list = [self._get_match(regex,string,flags)
                                for regex,string,flags in regex_list]

        logging.debug("%s match list",self.__class__.__name__)
        logging.debug("----------")
        for match in matchobject_list:
            if match:
                logging.debug("- %s",match.group())
            else:
                logging.debug("- %s",None)
        logging.debug("----------")

        return matchobject_list



    def get_forwarding_info(self):
        """
        Some MediaRegex expressions are dependant on information found in
        a previous expression. This return the information that the
        MediaRegex will forward to subsequent MediaRegex
        """
        return self.forward_dict

#---Utility

    def __str__(self):

        if not self.regex:
            return "%s: empty regex list"%self.__class__.__name__

        regex_list = ['(r"%s","%s","%s"'%(pattern,string,flag)
                        for pattern,string,flag in self.regex]

        return "\n".join(regex_list)



class ShownameRegex(MediaRegex):
    """
    Showname
    """
    def __init__(self):
        MediaRegex.__init__(self)
        self._add_regex(
                r"(?P<showname>[^${sep}]+)${sep}\W*(?=(Season|Series))",
                "${path}",re.IGNORECASE)

        self._add_regex(
                r"(?P<showname>.+?)\W+(?:Season|Series)[^${sep}]*?\d+",
                "${parent_dir}",re.IGNORECASE)

        self._add_regex(
                r"(?P<showname>[^${sep}]+)(Season|Series|[^A-Z]s)[\w\s]*\d+",
                "${path}",re.IGNORECASE)


        self._add_regex(r"(?P<showname>.*)","${parent_dir}")


    def _set_forwarded_info(self,match):
        """sets cut_filename, removes showname"""
        new_string = cut_from_string(match.group("showname"),self.filename)
        self._update_forwarding_info(cut_filename=new_string)


class SeasonRegex(MediaRegex):
    """
    Season Number
    """
    def __init__(self):
        MediaRegex.__init__(self)


        #handles splitting 101 into season 1 and leaving 01
        if keasby_cl.opts.assume_grouped_numbers:
            repeat_amount = r"+?((?=\d{%s})|(?=\D))"%keasby_cl.opts.grouped_num_split
        else:
            repeat_amount = r"+"


        self._add_regex(r"(?=(Season|Series))\D*(?P<season_number>\d+)",
                        "${parent_dir}",re.IGNORECASE)

        self._add_regex(r"(?<=[-\ _xs\.])[^\d()]?(?P<season_number>\d+)",
                        "${parent_dir}",re.IGNORECASE)

        self._add_regex(r"(?P<season_number>^\d${repeat_amount})",
                        "${cut_filename}",re.IGNORECASE)

        self._add_regex(
                r"(?<=(Season))\D*(?P<season_number>\d${repeat_amount})",
                "${cut_filename}",re.IGNORECASE)

        self._add_regex(
                r"(?<=[-\ _xs\.])(?P<season_number>\d${repeat_amount})",
                "${cut_filename}",re.IGNORECASE)

        if keasby_cl.opts.assume_grouped_numbers:
            pattern = r"+?((?=\d{%s})|(?=\D))"%keasby_cl.opts.grouped_num_split
        else:
            pattern = r"+"

        self.seed_dict(repeat_amount=pattern)

    def _set_forwarded_info(self,match):

        remaining_numbers=re.findall(r"\d+", self.cut_filename)

        logging.debug("leftover numbers: %s",remaining_numbers)

        if (len(remaining_numbers) > 1
            or len(remaining_numbers[0]) > keasby_cl.opts.grouped_num_split):

            new_filename = cut_from_string(match.group("season_number"),
                                            self.cut_filename)

            logging.debug("cutting number")

            self._update_forwarding_info(cut_filename=new_filename)

        else:
            logging.debug("assuming that remaining number shouldn't be cut")
            self._update_forwarding_info(cut_filename=self.cut_filename)



class EpisodeRegex(MediaRegex):
    """
    Episode Number
    """
    def __init__(self):
        MediaRegex.__init__(self)
        self._add_regex(
                    r"(?<=e)(?P<episode_number>\d+(?:\${split_char}+\d+)*)",
                        "${filename}",re.IGNORECASE)

        self._add_regex(
            r"(?<=[\ _xe\.])(?P<episode_number>\d+(?:\${split_char}+\d+)*)",
            "${cut_filename}",re.IGNORECASE)

        self._add_regex(r"(?P<episode_number>\d+(?:\${split_char}+\d+)*)",
                    "${cut_filename}",re.IGNORECASE)

        self.seed_dict(split_char=keasby_cl.opts.split_episode_char)


class MediaInterpreterError(Exception):
    """
    MediaInterpeter error
    """
    pass

class MediaRegexError(Exception):
    """
    MediaRegex error
    """
    pass

#---Unit Tests

class MediaInterpreterTestCase(KeasbyTestCase):
    """
    Tests the results of MediaInterpreter, given a series of media files,
    (self.media_list) and expected results (self.expected).
    """

    def setUp(self):
        """
        Instantiates the MediaInterpreter instance used by the test case
        """

        self.i = MediaInterpreter()

    def test_interpreted_results(self):
        """
        Tests the match of each interpreted result, compared to the
        expected result.
        """

        for answers,media_file in zip(self.expected,self.media_list):
            self.assertDictEquals(answers,self.i.get_groups(media_file))

    def test_interpreted_results_list(self):
        """
        Tests the match of each interpreted result, compared to the
        expected result. Using get_groups_list
        """

        found_groups_list = self.i.get_groups_list(self.media_list)
        self.assertEquals(len(self.media_list),len(found_groups_list))

        for media_file,found_groups in zip(self.expected, found_groups_list):
            self.assertDictEquals(media_file,found_groups)

    def test_preseeded_results_list(self):
        """
        Tests the same as behavior as test_interpreted_results_list, but
        pre-seeds the media_list
        """

        self.i.seed_media_list(self.media_list)

        found_groups_list = self.i.get_groups_list()
        self.assertEquals(len(self.media_list),len(found_groups_list))

        for media_file,found_groups in zip(self.expected, found_groups_list):
            self.assertDictEquals(media_file,found_groups)


class MediaInterpreterInternalTestCase(KeasbyTestCase):
    """
    Tests the internal workings of the MediaInterpreter
    The Normal Unit Test should no call any non-public function
    """

    def setUp(self):
        """
        Instantiates MediaInterpreter and seeds with the base media list
        """

        self.i = MediaInterpreter()
        self.i.seed_media_list(self.media_list)

    def test_seed_media(self):
        """
        Tests initialization of the media list used to generate
        information. Uses a basic, hardcoded test
        """

        media_file = pjoin("parent_of_parent_dir",
                            "parent_dir","filename.ext")

        expected = dict(path=media_file, ext=".ext", filename="filename",
                        parent_dir="parent_dir",
                        parent_of_parent_dir="parent_of_parent_dir")

        self.i.seed_media_list([media_file])

        self.assertDictEquals(expected, self.i.media_list[0])

    def test_match_validity(self):
        """
        Tests the correct creation of MatchObjects for the base
        media list
        """

        for regex_list in self.i._get_matches_list():
            for match in regex_list:
                self.assertLog(match,
                                pass_message="%s -> %s"%(match.string,
                                                        match.groupdict()),
                                fail_message=type(match),
                                pass_log=logging.debug)
            logging.debug("------")

    def test_match_equality(self):
        """
        Tests the correct creation of MatchObjects for the base
        media list, as well as their existance in the expected values
        dictionary
        """

        for regex_list,expctd in zip(self.i._get_matches_list(),self.expected):
            for match in regex_list:
                self.assertLog(match,"Not a valid match")
                _groupdict = match.groupdict()
                self.assertDictSubset(reshape_dict(_groupdict),expctd)

            logging.debug("------")

if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG,
                        format="%(levelname)s: %(message)s")

    unittest_list = []
    unittest_list.append(MediaInterpreterTestCase)
    unittest_list.append(MediaInterpreterInternalTestCase)

    for unittest_class in unittest_list:
        suite = unittest.TestLoader().loadTestsFromTestCase(unittest_class)
        unittest.TextTestRunner(verbosity=2).run(suite)